import os
import chardet
import time
import docx
import re
import numpy as np
import json
import uuid
import threading
import hashlib
from typing import List, Dict, Tuple, Any
from openai import OpenAI
from collections import Counter
from sklearn.feature_extraction.text import TfidfVectorizer
from sklearn.metrics.pairwise import cosine_similarity
import logging
from flask import Flask, request, jsonify
from flask_cors import CORS

# 配置日志
logging.basicConfig(level=logging.ERROR, format='%(asctime)s - %(levelname)s - %(message)s')

app = Flask(__name__)
CORS(app, supports_credentials=True, origins=["*"])


# 线程安全的机器人管理器
class ExamBotManager:
    def __init__(self):
        self.lock = threading.Lock()
        self.bots = {}

    def get_bot(self, session_id):
        with self.lock:
            return self.bots.get(session_id)

    def create_bot(self, session_id, config):
        with self.lock:
            bot = SmartExamBot(
                api_key=config["api_key"],
                base_url=config["base_url"],
                max_questions=config["max_questions"],
                knowledge_files=config["knowledge_files"]
            )
            self.bots[session_id] = bot
            return bot


exam_bot_manager = ExamBotManager()


class SmartExamBot:
    def __init__(self, api_key: str, base_url: str, max_questions: int = 5, knowledge_files: list = None):
        """初始化考试机器人"""
        self.api_key = api_key
        self.base_url = base_url
        self.client = OpenAI(
            api_key=api_key,
            base_url=base_url
        )
        self.max_follow_ups = 0
        self.question_count = 0
        self.total_score = 0
        self.max_possible_score = 0
        self.max_questions = max_questions
        self.current_follow_ups = 0
        self.question_scores = []
        self.cannot_answer_patterns = re.compile(
            r'不会|不知道|不清楚|没学过|不懂|不了解|不会做|不知道怎么答|无法回答|我举不出来例子|分析不出来',
            re.IGNORECASE
        )
        self.irrelevant_patterns = re.compile(
            r'跑题|偏题|无关|答非所问|换个题|另一个|不相关|说点别的|其他内容|别的话题',
            re.IGNORECASE
        )
        self.image_patterns = re.compile(
            r'如图|图示|图片|图表|图[.\d]+|figure|graph|chart|diagram|illustration|源程序',
            re.IGNORECASE
        )
        self.knowledge_content = ""
        self.keywords = []
        self.current_question = ""
        self.question_dimensions = []
        self.unrelated_topics = {'天气', '吃饭', '电影', '游戏', '睡觉', '时间', '日期',
                                 '音乐', '歌曲', '明星', '电影', '旅游', '购物', '美食',
                                 '体育', '运动', '小说', '书籍', '电视剧', '综艺'}
        self.knowledge_files = knowledge_files or []

        # RAG相关初始化
        self.vectorizer = TfidfVectorizer()
        self.knowledge_vectors = None
        self.knowledge_passages = []

        # 新增：历史题目记录
        self.generated_questions = []
        self.question_similarity_threshold = 0.7  # 题目相似度阈值

        # 新增：纯文字题目缓存池
        self.text_question_cache = []

        # 新增：敏感模式匹配
        self.sensitive_patterns = [
            r'<img\s+.*?>',  # HTML 图片标签
            r'!\[.*?\]\(.*?\)',  # Markdown 图片语法
            r'```(python|java|.*?)',  # 代码块标记
            r'<code>.*?</code>'  # HTML 代码块
        ]

    def reset_exam(self):
        """重置考试状态"""
        self.question_count = 0
        self.total_score = 0
        self.max_possible_score = 0
        self.question_scores = []
        self.current_question = ""
        self.generated_questions = []
        self.text_question_cache = []
        self.current_follow_ups = 0

    def load_knowledge(self):
        """从多个文件加载知识库内容"""
        full_content = ""
        for file_path in self.knowledge_files:
            try:
                content = self.read_file_content(file_path)
                full_content += content + "\n\n"
                print(f"已加载文件: {os.path.basename(file_path)}")
            except Exception as e:
                print(f"加载文件失败 {file_path}: {str(e)}")
        self.knowledge_content = full_content
        print(f"知识库总字数: {len(self.knowledge_content)}")
        # 提取关键词和向量化
        self.keywords = self._extract_keywords(self.knowledge_content)
        self._vectorize_knowledge()
        return self.knowledge_content

    def _chunk_knowledge(self, content: str, chunk_size: int = 500) -> List[str]:
        """将知识库内容分块处理"""
        chunks = []
        words = content.split()
        for i in range(0, len(words), chunk_size):
            chunk = " ".join(words[i:i + chunk_size])
            chunks.append(chunk)
        return chunks

    def _vectorize_knowledge(self):
        """向量化知识库内容"""
        if not self.knowledge_passages:
            self.knowledge_passages = self._chunk_knowledge(self.knowledge_content)
        self.vectorizer.fit(self.knowledge_passages)
        self.knowledge_vectors = self.vectorizer.transform(self.knowledge_passages)

    def retrieve_relevant_passages(self, query: str, top_k: int = 3) -> List[str]:
        """检索与查询最相关的知识库片段"""
        if self.knowledge_vectors is None:
            self._vectorize_knowledge()

        query_vec = self.vectorizer.transform([query])
        similarities = cosine_similarity(query_vec, self.knowledge_vectors)
        top_indices = np.argsort(similarities[0])[-top_k:][::-1]

        return [self.knowledge_passages[i] for i in top_indices]

    def _is_question_too_similar(self, new_question: str) -> bool:
        """检查新题目是否与历史题目过于相似"""
        if not self.generated_questions:
            return False

        # 向量化新题目
        new_vec = self.vectorizer.transform([new_question])

        # 计算与所有历史题目的相似度
        for old_question in self.generated_questions:
            old_vec = self.vectorizer.transform([old_question])
            similarity = cosine_similarity(new_vec, old_vec)[0][0]
            if similarity > self.question_similarity_threshold:
                return True
        return False

    def _extract_keywords(self, text: str) -> List[str]:
        """从知识库中提取关键词"""
        words = re.findall(r'\b[\u4e00-\u9fa5]{2,6}\b', text)
        word_counts = Counter(words)
        stop_words = {'可以', '一个', '这个', '我们', '进行', '需要', '应该', '问题', '方法'}
        keywords = [word for word, count in word_counts.most_common(20)
                    if word not in stop_words and count > 1]
        return keywords[:10] if keywords else ["数据", "分析", "技术"]

    def _extract_dimensions(self, question: str) -> List[str]:
        """提取题目要求的分析维度"""
        match = re.search(r'请从(.*?)分析', question)
        if match:
            dimensions_text = match.group(1)
            dimensions = [dim.strip() for dim in re.split(r'、|和|与', dimensions_text) if dim.strip()]
            return dimensions
        return []

    def _is_duplicating_question(self, question: str, answer: str) -> bool:
        """检测回答是否只是重复问题内容"""
        if not question or not answer:
            return False

        def preprocess(text):
            text = re.sub(r'[^\w\s]', '', text)
            text = re.sub(r'请从|分析|简述|说明|论述|谈谈|如何|什么|为什么|是什么|有哪些', '', text,
                          flags=re.IGNORECASE)
            return text.strip()

        processed_question = preprocess(question)
        processed_answer = preprocess(answer)

        if len(processed_answer) < len(processed_question) * 0.5:
            return True

        common_chars = 0
        min_len = min(len(processed_question), len(processed_answer))
        for i in range(min_len):
            if processed_question[i] == processed_answer[i]:
                common_chars += 1

        if common_chars / min_len > 0.3:
            return True

        question_words = set(re.findall(r'\b[\u4e00-\u9fa5a-zA-Z]{2,}\b', processed_question))
        answer_words = set(re.findall(r'\b[\u4e00-\u9fa5a-zA-Z]{2,}\b', processed_answer))

        if question_words and len(answer_words.intersection(question_words)) / len(question_words) > 0.7:
            return True

        return True if processed_answer in processed_question or processed_question in processed_answer else False

    def detect_encoding(self, file_path: str) -> str:
        """检测文件编码"""
        if file_path.lower().endswith('.docx'):
            return 'docx'
        try:
            with open(file_path, 'rb') as file:
                result = chardet.detect(file.read(10240))
                return result['encoding'] or 'utf-8'
        except Exception as e:
            raise Exception(f"编码检测失败: {str(e)}")

    def read_file_content(self, file_path: str) -> str:
        """读取文件内容，支持txt和docx格式"""
        if not os.path.exists(file_path):
            raise FileNotFoundError(f"文件不存在: {file_path}")

        if not os.path.isfile(file_path):
            raise IsADirectoryError(f"{file_path}不是一个文件")

        try:
            encoding = self.detect_encoding(file_path)

            if encoding == 'docx':
                doc = docx.Document(file_path)
                full_text = []
                for para in doc.paragraphs:
                    full_text.append(para.text)
                for table in doc.tables:
                    for row in table.rows:
                        row_text = [cell.text for cell in row.cells]
                        full_text.append('\t'.join(row_text))
                return '\n'.join(full_text)

            encodings_to_try = [encoding, 'utf-8', 'gbk', 'latin-1']
            for enc in encodings_to_try:
                try:
                    with open(file_path, 'r', encoding=enc, errors='replace') as file:
                        return file.read()
                except UnicodeDecodeError:
                    continue
            raise Exception("无法识别文件编码")
        except Exception as e:
            raise Exception(f"文件读取失败: {str(e)}")

    def get_ai_response(self, messages: List[Dict], max_retries: int = 3) -> str:
        """获取AI响应，带重试机制"""
        retry_count = 0
        while retry_count < max_retries:
            try:
                response = self.client.chat.completions.create(
                    model="deepseek-v3",
                    messages=messages,
                    temperature=0.7,  # 提高温度值以增加多样性
                    timeout=30
                )
                return response.choices[0].message.content
            except Exception as e:
                retry_count += 1
                if retry_count >= max_retries:
                    raise Exception(f"API请求失败: {str(e)}")
                print(f"API调用失败，正在重试({retry_count}/{max_retries})...")
                time.sleep(2)

    def is_cannot_answer(self, user_input: str) -> bool:
        """判断用户输入是否表示'不会'"""
        user_input = user_input.strip()
        if not user_input or re.fullmatch(r'[\s,.!?;]+', user_input):
            return True
        return self.cannot_answer_patterns.search(user_input) is not None

    def is_irrelevant(self, user_answer: str) -> Tuple[bool, str]:
        """判断回答是否与题目和知识库无关（乱答判定）"""
        user_answer = user_answer.strip().lower()

        if self.irrelevant_patterns.search(user_answer):
            return True, "回答明确表示与题目无关"

        question_keywords = re.findall(r'\b[\u4e00-\u9fa5]{2,6}\b', self.current_question)
        all_keywords = question_keywords + self.keywords + self.question_dimensions

        has_relevant_keywords = any(keyword in user_answer for keyword in all_keywords)

        if not has_relevant_keywords:
            for topic in self.unrelated_topics:
                if topic in user_answer:
                    return True, f"回答涉及无关主题({topic})，视为乱答"

            if len(user_answer) < 5:
                return True, "回答过短且不涉及题目相关内容，视为乱答"

            return True, "回答与题目及知识库内容无关，视为乱答"

        return False, ""

    def need_regenerate(self, content):
        """检查内容是否包含敏感模式"""
        for pattern in self.sensitive_patterns:
            if re.search(pattern, content, re.IGNORECASE):
                return True
        return False

    def generate_question(self):
        """生成题目 - 适配前端API"""
        if not hasattr(self, 'messages') or not self.messages:
            # 初始化消息
            self.messages = [
                {"role": "system", "content": f"""
                你是一个公平的智能考试机器人，需要基于以下知识库内容出题和评分：
                {self.knowledge_content[:2000]}...

                工作模式：

                1. 题目生成模式：
                - 必须从知识库中提取具体概念出题
                - 题目必须包含至少1个关键词：{', '.join(self.keywords)}
                - 必须要求从1-2个维度回答
                - 示例："大语言模型在学生学业中正面作用是什么?它可能带来什么危害?学生如何避免"
                - 只返回题目，不要包含评分信息、评分标准等其他任何信息
                - 绝对不能包含任何图片、图示相关的内容
                - 题目必须多样化，避免与已出题目相似
                - 不要给出回答建议
                - 尽量从不同角度提问
                - 请生成纯文字题目，无需包含图片、代码块。题目内容需符合：
                  - 仅用文字描述技术问题
                  - 避免使用 <img>、``` 等语法
                  - 用文字表述代码逻辑（如"编写一个 Python 函数实现排序"而非直接贴代码）
                """},
            ]

        # 生成题目逻辑
        if self.question_count >= self.max_questions:
            return None

        # 尝试从缓存中获取题目
        if self.text_question_cache:
            question = self.text_question_cache.pop(0)
            if not self._is_question_too_similar(question):
                self.current_question = question
                self.question_dimensions = self._extract_dimensions(question)
                self.generated_questions.append(question)
                self.messages.append({"role": "assistant", "content": question})
                self.max_possible_score += 10
                self.question_count += 1
                return question

        # 生成新题目
        max_retries = 3
        retry_count = 0
        question = None

        while retry_count < max_retries:
            # RAG增强：检索与关键词最相关的知识片段
            context_query = " ".join(self.keywords[:3])
            relevant_passages = self.retrieve_relevant_passages(context_query, top_k=2)
            context = "\n\n".join(relevant_passages)

            prompt = f"""请基于以下知识库内容出一道新颖的主观题（避免与已出题目相似）：
            知识库相关段落：
            {context[:1500]}

            要求：
            1. 必须从以下关键词中选择至少1个：{', '.join(self.keywords)}
            2. 题目形式应为：
               "请从[维度1]分析[关键词]..."
            3. 确保题目在知识库中有明确答案依据
            4. 题目必须与前{len(self.generated_questions)}道已出题目有明显区别
            5. 从不同角度提问（如：定义、应用、优缺点、案例分析等）
            6. 仅输出题目内容，不要输出"评分标准"等其他所有内容、过滤额外说明内容
            7. 绝对不要包含任何图片、图示、源程序等描述
            8. 请生成纯文字题目，无需包含图片、代码块。题目内容需符合：
               - 仅用文字描述技术问题
               - 避免使用 <img>、``` 等语法
               - 用文字表述代码逻辑（如"编写一个 Python 函数实现排序"而非直接贴代码）
            """

            self.messages.append({"role": "user", "content": prompt})
            question = self.get_ai_response(self.messages)
            self.messages.pop()  # 移除用户提示

            # 验证题目质量
            validation_failed = False

            # 1. 检查是否包含关键词
            if not any(keyword in question for keyword in self.keywords):
                print("⚠️ 题目未包含关键词，重新生成...")
                validation_failed = True

            # 2. 检查是否要求多维度回答
            elif len(re.findall(r'维度|角度|方面', question)) < 1:
                print("⚠️ 题目未明确要求多维度回答，重新生成...")
                validation_failed = True

            # 3. 检查是否包含图片/源程序描述
            elif self.need_regenerate(question):
                print("⚠️ 题目包含图片/代码嵌入语法，重新生成...")
                validation_failed = True

            # 4. 检查题目长度
            elif not question or len(question) < 10:
                print("⚠️ 题目过短，重新生成...")
                validation_failed = True

            # 5. 检查题目相似度
            elif self._is_question_too_similar(question):
                print("⚠️ 题目与历史题目过于相似，重新生成...")
                validation_failed = True

            if validation_failed:
                retry_count += 1
                continue

            # 题目通过所有验证，存入缓存
            self.text_question_cache.append(question)
            break

        if retry_count >= max_retries or not question:
            logging.error("无法生成纯文字题，启用备用库")
            # 备用题目生成逻辑
            question = f"请分析{self.keywords[0]}在数据科学中的应用场景和主要挑战"
            print(f"⚠️ 使用备用题目: {question}")

        # 清理题目格式
        question = question.split("|")[0].strip()
        question = re.sub(r'^\d+[.\d]*\s*', '', question)

        # 保存当前题目和提取的维度
        self.current_question = question
        self.question_dimensions = self._extract_dimensions(question)
        self.generated_questions.append(question)  # 记录已生成题目
        self.messages.append({"role": "assistant", "content": question})
        self.max_possible_score += 10
        self.question_count += 1
        return question

    def evaluate_answer(self, user_answer: str) -> dict:
        """评估用户答案 - 适配前端API"""
        # 0. 检查是否无法回答
        if self.is_cannot_answer(user_answer):
            return {
                "score": 0,
                "feedback": "检测到无法回答的表述，本题需要按照题目要求的多个维度回答",
                "reference": "",
                "total_score": self.total_score,
                "average_score": self.total_score / self.question_count if self.question_count else 0,
                "question_count": self.question_count,
                "max_questions": self.max_questions
            }

        # 1. 检查是否无关回答
        is_irrelevant, reason = self.is_irrelevant(user_answer)
        if is_irrelevant:
            return {
                "score": 0,
                "feedback": reason,
                "reference": "",
                "total_score": self.total_score,
                "average_score": self.total_score / self.question_count if self.question_count else 0,
                "question_count": self.question_count,
                "max_questions": self.max_questions
            }

        # 2. 检查是否重复题目
        if self._is_duplicating_question(self.current_question, user_answer):
            return {
                "score": 0,
                "feedback": "回答只是重复或修改题目内容，请提供具体分析和解释",
                "reference": "",
                "total_score": self.total_score,
                "average_score": self.total_score / self.question_count if self.question_count else 0,
                "question_count": self.question_count,
                "max_questions": self.max_questions
            }

        # 3. 维度检查
        answer_dimensions = self._extract_dimensions(user_answer)
        missing_dimensions = set(self.question_dimensions) - set(answer_dimensions)
        dimension_penalty = 0

        if missing_dimensions and len(answer_dimensions) > 0:
            dimension_penalty = len(missing_dimensions) / len(self.question_dimensions)
            print(f"⚠️ 回答未完全覆盖题目要求的维度（缺少: {', '.join(missing_dimensions)}）")

        # 4. 添加用户答案到消息历史
        self.messages.append({"role": "user", "content": user_answer})

        # 5. 检索相关知识
        query = f"{self.current_question}\n{user_answer}"
        relevant_passages = self.retrieve_relevant_passages(query, top_k=3)
        context = "\n\n".join(relevant_passages)

        # 6. 获取AI评分
        evaluation = self.get_ai_response(self.messages + [
            {"role": "system", "content": f"""
            请基于以下相关知识和评分标准进行评分：

            ===== 相关知识片段 =====
            {context[:2000]}

            ===== 评分标准 =====
            1. 知识库关键内容：{self.knowledge_content[:1000]}
            2. 题目要求维度：{', '.join(self.question_dimensions)}
            3. 评分标准：
               10分：全面覆盖所有维度，准确引用知识库内容，逻辑性非常强
               8-9分：覆盖主要维度，正确引用知识库内容，有一定深度，逻辑性较强
               6-7分：部分覆盖维度，与知识库有一定关联，内容较简单，逻辑性一般
               4-5分：答案仅涉及少量关键点且逻辑性一般，相关性较弱
               0分：未涉及题目核心内容或乱答，无逻辑性

            请按以下格式提供评分反馈：
                分数：[给出0-10分的具体分数]
                反馈：[详细说明评分理由，指出回答中的优缺点]
                改进建议：[提供具体的改进方向和学习建议]
            """},
        ])

        # 7. 解析评分
        score = 6.0  # 默认分数
        feedback = "无法解析评分结果，按中等分数处理"
        suggestion = "请确保回答覆盖题目要求的所有维度"

        try:
            # 尝试从自由文本中提取分数和反馈
            score_match = re.search(r'(\d+(?:\.\d+)?)\s*分(?!钟)', evaluation)
            if score_match:
                score = float(score_match.group(1))
                score = max(0, min(10, score))

            # 提取反馈部分
            feedback_match = re.search(r'(反馈|评价|评语)[:：]?(.+?)(?=(改进建议|建议|$))', evaluation, re.DOTALL)
            if feedback_match:
                feedback = feedback_match.group(2).strip()

            # 提取改进建议
            suggestion_match = re.search(r'(改进建议|建议)[:：]?(.+)', evaluation, re.DOTALL)
            if suggestion_match:
                suggestion = suggestion_match.group(2).strip()

            print(f"成功解析评分: {score:.1f}分, 反馈: {feedback[:50]}...")
        except Exception as e:
            print(f"⚠️ 评分解析错误: {str(e)}")

        # 8. 维度缺失惩罚
        if dimension_penalty > 0:
            score_deduction = min(2, dimension_penalty * 2)
            score = max(0, score - score_deduction)
            feedback = f"回答未完全覆盖所有维度（缺少: {', '.join(missing_dimensions)}），扣{score_deduction:.1f}分。{feedback}"
            print(f"维度缺失惩罚: 扣{score_deduction:.1f}分")

        # 9. 更新总分
        self.total_score += score
        self.question_scores.append(score)

        # 10. 添加到消息历史
        self.messages.append({"role": "assistant", "content": f"{score:.1f}|{feedback}|{suggestion}"})

        # 11. 生成参考答案
        reference = self.generate_reference_answer()

        return {
            "score": score,
            "feedback": feedback,
            "suggestion": suggestion,
            "reference": reference,
            "total_score": self.total_score,
            "average_score": self.total_score / self.question_count if self.question_count else 0,
            "question_count": self.question_count,
            "max_questions": self.max_questions
        }

    def generate_reference_answer(self) -> str:
        """生成当前题目的参考答案"""
        if not self.current_question:
            return ""

        prompt = f"""
        根据以下题目和知识库生成标准答案：
        题目：{self.current_question}
        知识库：{self.knowledge_content[:2000]}

        要求：
        1. 只返回答案内容
        2. 分点列出关键要素
        3. 不要包含评分信息
        """

        try:
            response = self.get_ai_response([
                {"role": "system", "content": prompt}
            ])
            return response
        except:
            return "无法生成参考答案"


# Flask路由
@app.route('/start_exam', methods=['POST'])
def start_exam():
    """开始考试"""
    session_id = request.cookies.get('session_id') or str(uuid.uuid4())

    # 配置信息
    CONFIG = {
        "api_key": "sk-ce186a5b911341ba983baf0bf1ee2904",
        "base_url": "https://dashscope.aliyuncs.com/compatible-mode/v1",
        "max_questions": 5,
        "knowledge_files": [
            r"E:/第3章 数据获取.docx",
            # 可以添加更多文件
            # r"其他文件路径.docx",
        ]
    }

    bot = exam_bot_manager.create_bot(session_id, CONFIG)
    bot.reset_exam()

    try:
        # 加载知识库
        bot.load_knowledge()

        # 生成第一题
        question = bot.generate_question()

        if not question:
            return jsonify({"error": "题目生成失败"}), 500

        response = jsonify({
            "question": question,
            "question_count": bot.question_count,
            "max_questions": bot.max_questions,
            "knowledge_files": [os.path.basename(f) for f in CONFIG["knowledge_files"]]
        })
        response.set_cookie('session_id', session_id, httponly=True, samesite='None', secure=True)
        return response

    except Exception as e:
        return jsonify({"error": str(e)}), 500


@app.route('/submit_answer', methods=['POST'])
def submit_answer():
    """提交答案"""
    session_id = request.cookies.get('session_id')
    if not session_id:
        return jsonify({"error": "缺少会话ID"}), 400

    bot = exam_bot_manager.get_bot(session_id)
    if not bot:
        return jsonify({"error": "考试未开始"}), 400

    data = request.get_json()
    if not data or "answer" not in data:
        return jsonify({"error": "需要提供答案内容"}), 400

    user_answer = data["answer"]
    result = bot.evaluate_answer(user_answer)

    return jsonify(result)


@app.route('/next_question', methods=['GET'])
def next_question():
    """获取下一题"""
    session_id = request.cookies.get('session_id')
    if not session_id:
        return jsonify({"error": "缺少会话ID"}), 400

    bot = exam_bot_manager.get_bot(session_id)
    if not bot:
        return jsonify({"error": "考试未开始"}), 400

    if bot.question_count >= bot.max_questions:
        # 返回考试完成信息和所有得分
        return jsonify({
            "completed": True,
            "total_score": bot.total_score,
            "max_possible_score": bot.max_questions * 10,
            "question_scores": bot.question_scores
        })

    question = bot.generate_question()
    if not question:
        return jsonify({"error": "题目生成失败"}), 500

    return jsonify({
        "question": question,
        "question_count": bot.question_count,
        "max_questions": bot.max_questions
    })


if __name__ == '__main__':
    app.run(host='0.0.0.0', port=5000, debug=True)